import {pipe, filter, accumulate, forEach} from './iterator.js'
import {createObjectMap} from './utils/createObjectMap.js'

export type TokenType =
    | ':' | '::'
    | 'environ' | 'text'
    | 'relations' | 'operations' | 'precedence' | 'brackets'
    | 'type' | 'reserve' | 'pred' | 'func' | 'constructor' | '->'
    | 'by'
    | 'proof' | 'qed'
    | 'begin' | 'end'
    | 'assume' | 'let' | 'given' | 'consider' | 'take' | 'thus' | 'then' | 'hence' | 'such' | 'that' | 'and'
    | 'ex' | 'st' | 'for' | 'holds' | 'be' | 'is'
    | '&' | 'or' | 'iff' | 'implies' | 'not'
    | 'true' | 'false' | 'thesis'
    | '[' | ']' | '(' | ')' | ',' | '=' | ';'
    | '_symbol_' | '_identifier_' | '_number_' | '_comment_' | '_space_' | '_char_'
    | '_eof_'

const tokenMap = new Map<string, TokenType>([
    [':', ':'],
    ['::', '::'],
    ['environ', 'environ'],
    ['text', 'text'],
    ['relations', 'relations'],
    ['operations', 'operations'],
    ['precedence', 'precedence'],
    ['brackets', 'brackets'],
    ['type', 'type'],
    ['reserve', 'reserve'],
    ['pred', 'pred'],
    ['func', 'func'],
    ['constructor', 'constructor'],
    ['->', '->'],
    ['by', 'by'],
    ['proof', 'proof'],
    ['qed', 'qed'],
    ['begin', 'begin'],
    ['end', 'end'],
    ['assume', 'assume'],
    ['let', 'let'],
    ['given', 'given'],
    ['consider', 'consider'],
    ['take', 'take'],
    ['thus', 'thus'],
    ['then', 'then'],
    ['hence', 'hence'],
    ['such', 'such'],
    ['that', 'that'],
    ['and', 'and'],
    ['ex', 'ex'],
    ['st', 'st'],
    ['for', 'for'],
    ['holds', 'holds'],
    ['be', 'be'],
    ['being', 'be'],
    ['is', 'is'],
    ['&', '&'],
    ['or', 'or'],
    ['iff', 'iff'],
    ['implies', 'implies'],
    ['not', 'not'],
    ['true', 'true'],
    ['false', 'false'],
    ['contradiction', 'false'],
    ['thesis', 'thesis'],
    ['[', '['],
    [']', ']'],
    ['(', '('],
    [')', ')'],
    [',', ','],
    ['=', '='],
    [';', ';'],
])

const identifierRE = /[\wА-я]/

export type Token = {
    index: number
    type: TokenType
    string: string
}

export class Tokenizer {
    text: string
    length: number
    i = 0
    symbols = createObjectMap<string, TokenType | false>()

    constructor(articleText: string) {
        this.text = articleText
        this.length = this.text.length

        pipe(
            tokenMap.values(),
            filter(token => /^\W/.test(token)),
            forEach(token => this.addSymbol(token, token)),
        )
    }

    addSymbol(name: string, token: TokenType = '_symbol_') {
        pipe(
            name,
            accumulate((prefix, char) => prefix + char, ''),
            forEach(prefix => this.symbols[prefix] ||= false),
        )

        this.symbols[name] = token

        return this
    }

    setSourceIndex(index: number) {
        this.i = index
    }

    [Symbol.iterator](): Iterator<Token, Token> {
        return this
    }

    next(): IteratorResult<Token, Token> {
        const index = this.i

        if (this.i >= this.length)
            return {
                value: {
                    index,
                    type: '_eof_',
                    string: '',
                },
                done: true,
            }

        let char = this.text[this.i]
        let token = ''

        while (/\s/.test(char) && this.i < this.length) {
            token += char
            char = this.text[++this.i]
        }

        if (token)
            return iteratorResult({
                index,
                type: '_space_',
                string: token,
            })

        while (token + char in this.symbols && this.i < this.length) {
            token += char
            char = this.text[++this.i]
        }

        if (token) {
            let tokenType = this.symbols[token] || tokenMap.get(token)

            if (tokenType) {
                if (tokenType === '::') {
                    while (!/[\n\r]/.test(char) && this.i < this.length) {
                        token += char
                        char = this.text[++this.i]
                    }

                    tokenType = '_comment_'
                }

                return iteratorResult({
                    index,
                    type: tokenType,
                    string: token,
                })
            }

            this.i = index
            char = this.text[this.i]
            token = ''
        }

        while (/\d/.test(char) && this.i < this.length) {
            token += char
            char = this.text[++this.i]
        }

        if (token) {
            const tokenType = tokenMap.get(token)

            return iteratorResult({
                index,
                type: tokenType || '_number_',
                string: token,
            })
        }

        while (identifierRE.test(char) && this.i < this.length) {
            token += char
            char = this.text[++this.i]
        }

        if (token) {
            const tokenType = tokenMap.get(token)

            return iteratorResult({
                index,
                type: tokenType || '_identifier_',
                string: token,
            })
        }

        this.i++

        return iteratorResult({
            index,
            type: '_char_',
            string: char,
        })
    }
}

const iteratorResult = <T>(value: T): IteratorResult<T> => ({
    value,
    done: false,
})

export function getLineDataFromIndex(offset: number, string: string) {
    let lineNumber = 1
    let line = ''

    for (const char of string) {
        if (char == '\n') {
            if (offset <= 0)
                break

            lineNumber++
            line = ''
        } else
            line += char

        offset--
    }

    return {
        lineNumber,
        offset: line.length + offset,
        line,
    }
}

// const article = `
//     environ
//     type Human; type Shoes;
//     func s() -> Shoes; func w() -> Human;
//     pred F[Human]; pred G[Human]; pred H[Human];
//     1: A[s()];
//     5: B s implies C s & D s or not F w;
//
//     reserve x for Human;
//     7: for x st A[s()] & not G[x] holds H[x];
//
//     text
//
//     ::H[w()] from 1,2,3,4,5,6,7; ::-mpd:3 -mml:4
//
//     8: H[w()]
//     proof
//         8: A[s()] & not G[w()] implies H[w()] by 7;
//         9: not G[w()]; :: sorry
//         10: A[s()] & not G[w()] by 1,9;
//         thus 11: H[w()] by 8,10;
//     qed;
// `;
//
// pipe(
//     new Tokenizer(article),
//     // new Tokenizer('x<=::=>y<=A').addSymbol('<=:=>'),
//     map(({token, string, index}) => `${index} ${string} ${token}`),
//     forEach(console.log)
// );
